/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.common;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.apache.solr.common.util.CollectionUtil;
import org.apache.solr.common.util.NamedList;

/**
 * A concrete representation of a document within a Solr index. Unlike a lucene Document, a
 * SolrDocument may have an Object value matching the type defined in schema.xml
 *
 * <p>For indexing documents, use the SolrInputDocument that contains extra information for document
 * and field boosting.
 *
 * @since solr 1.3
 */
public class SolrDocument extends SolrDocumentBase<Object, SolrDocument>
    implements Iterable<Map.Entry<String, Object>> {
  protected final Map<String, Object> _fields;

  private List<SolrDocument> _childDocuments;

  public SolrDocument() {
    _fields = new LinkedHashMap<>();
  }

  @Override
  public void writeMap(EntryWriter ew) throws IOException {
    _fields.forEach(ew.getBiConsumer());
  }

  public SolrDocument(Map<String, Object> fields) {
    this._fields = fields;
  }

  /**
   * @return a list of field names defined in this document - this Collection is directly backed by
   *     this SolrDocument.
   * @see #keySet
   */
  @Override
  public Collection<String> getFieldNames() {
    return this.keySet();
  }

  ///////////////////////////////////////////////////////////////////
  // Add / Set / Remove Fields
  ///////////////////////////////////////////////////////////////////

  /** Remove all fields from the document */
  @Override
  public void clear() {
    _fields.clear();

    if (_childDocuments != null) {
      _childDocuments.clear();
    }
  }

  /** Remove all fields with the name */
  public boolean removeFields(String name) {
    return this.remove(name) != null;
  }

  /**
   * Set a field with the given object. If the object is an Array, it will set multiple fields with
   * the included contents. This will replace any existing field with the given name
   */
  @Override
  public void setField(String name, Object value) {
    if (value instanceof Object[]) {
      value = new ArrayList<>(Arrays.asList((Object[]) value));
    } else if (value instanceof Collection) {
      // nothing
    } else if (value instanceof NamedList) {
      // nothing
    } else if (value instanceof Iterable && !(value instanceof SolrDocumentBase)) {
      ArrayList<Object> lst = new ArrayList<>();
      for (Object o : (Iterable) value) {
        lst.add(o);
      }
      value = lst;
    }
    _fields.put(name, value);
  }

  /**
   * This will add a field to the document. If fields already exist with this name it will append
   * value to the collection. If the value is Collection, each value will be added independently.
   *
   * <p>The class type of value and the name parameter should match schema.xml. schema.xml can be
   * found in conf directory under the solr home by default.
   *
   * @param name Name of the field, should match one of the field names defined under "fields" tag
   *     in schema.xml.
   * @param value Value of the field, should be of same class type as defined by "type" attribute of
   *     the corresponding field in schema.xml.
   */
  @SuppressWarnings("unchecked")
  @Override
  public void addField(String name, Object value) {
    Object existing = _fields.get(name);
    if (existing == null) {
      if (value instanceof Collection) {
        Collection<Object> c = new ArrayList<>(3);
        for (Object o : (Collection<Object>) value) {
          c.add(o);
        }
        this.setField(name, c);
      } else {
        this.setField(name, value);
      }
      return;
    }

    Collection<Object> vals = null;
    if (existing instanceof Collection) {
      vals = (Collection<Object>) existing;
    } else {
      vals = new ArrayList<>(3);
      vals.add(existing);
    }

    // Add the values to the collection
    if (value instanceof Iterable && !(value instanceof SolrDocumentBase)) {
      for (Object o : (Iterable<Object>) value) {
        vals.add(o);
      }
    } else if (value instanceof Object[]) {
      for (Object o : (Object[]) value) {
        vals.add(o);
      }
    } else {
      vals.add(value);
    }
    _fields.put(name, vals);
  }

  ///////////////////////////////////////////////////////////////////
  // Get the field values
  ///////////////////////////////////////////////////////////////////

  /** returns the first value for a field */
  public Object getFirstValue(String name) {
    Object v = _fields.get(name);
    if (v == null || !(v instanceof Collection<?> c)) return v;
    if (c.size() > 0) {
      return c.iterator().next();
    }
    return null;
  }

  /** Get the value or collection of values for a given field. */
  @Override
  public Object getFieldValue(String name) {
    return _fields.get(name);
  }

  /** Get a collection of values for a given field name */
  @SuppressWarnings("unchecked")
  @Override
  public Collection<Object> getFieldValues(String name) {
    Object v = _fields.get(name);
    if (v instanceof Collection) {
      return (Collection<Object>) v;
    }
    if (v != null) {
      ArrayList<Object> arr = new ArrayList<>(1);
      arr.add(v);
      return arr;
    }
    return null;
  }

  /** Get the value or collection of values for a given field. */
  public Map<String, Object> getSubsetOfFields(Set<String> fieldNames) {
    final HashMap<String, Object> subset = CollectionUtil.newHashMap(fieldNames.size());
    fieldNames.forEach(
        f -> {
          Object v = getFieldValue(f);
          if (v != null) {
            subset.put(f, getFieldValue(f));
          }
        });
    return subset;
  }

  @Override
  public String toString() {
    return "SolrDocument" + _fields;
  }

  /** Iterate of String-&gt;Object keys */
  @Override
  public Iterator<Entry<String, Object>> iterator() {
    return _fields.entrySet().iterator();
  }

  /** Beta API; may change at will. */
  // TODO SOLR-15063 reconcile SolrDocumentBase/SolrDocument/SolrInputDocument debacle
  public void visitSelfAndNestedDocs(BiConsumer<String, SolrDocument> consumer) {
    consumer.accept(null, this);
    for (Entry<String, Object> keyVal : entrySet()) {
      final Object value = keyVal.getValue();
      if (value instanceof SolrDocument) {
        consumer.accept(keyVal.getKey(), (SolrDocument) value);
      } else if (value instanceof Collection<?> cVal) {
        for (Object v : cVal) {
          if (v instanceof SolrDocument) {
            consumer.accept(keyVal.getKey(), (SolrDocument) v);
          } else {
            break; // either they are all SolrDocs, or none are
          }
        }
      }
    }

    if (_childDocuments != null) {
      for (SolrDocument childDocument : _childDocuments) {
        consumer.accept(null, childDocument);
      }
    }
  }

  // -----------------------------------------------------------------------------------------
  // JSTL Helpers
  // -----------------------------------------------------------------------------------------

  /** Expose a Map interface to the solr field value collection. */
  public Map<String, Collection<Object>> getFieldValuesMap() {
    return new Map<>() {
      /** Get the field Value */
      @Override
      public Collection<Object> get(Object key) {
        return getFieldValues((String) key);
      }

      // Easily Supported methods
      @Override
      public boolean containsKey(Object key) {
        return _fields.containsKey(key);
      }

      @Override
      public Set<String> keySet() {
        return _fields.keySet();
      }

      @Override
      public int size() {
        return _fields.size();
      }

      @Override
      public boolean isEmpty() {
        return _fields.isEmpty();
      }

      // Unsupported operations.  These are not necessary for JSTL
      @Override
      public void clear() {
        throw new UnsupportedOperationException();
      }

      @Override
      public boolean containsValue(Object value) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Set<java.util.Map.Entry<String, Collection<Object>>> entrySet() {
        throw new UnsupportedOperationException();
      }

      @Override
      public void putAll(Map<? extends String, ? extends Collection<Object>> t) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Collection<Collection<Object>> values() {
        throw new UnsupportedOperationException();
      }

      @Override
      public Collection<Object> put(String key, Collection<Object> value) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Collection<Object> remove(Object key) {
        throw new UnsupportedOperationException();
      }

      @Override
      public String toString() {
        return _fields.toString();
      }
    };
  }

  /** Expose a Map interface to the solr fields. This function is useful for JSTL */
  public Map<String, Object> getFieldValueMap() {
    return new Map<>() {
      /** Get the field Value */
      @Override
      public Object get(Object key) {
        return getFirstValue((String) key);
      }

      // Easily Supported methods
      @Override
      public boolean containsKey(Object key) {
        return _fields.containsKey(key);
      }

      @Override
      public Set<String> keySet() {
        return _fields.keySet();
      }

      @Override
      public int size() {
        return _fields.size();
      }

      @Override
      public boolean isEmpty() {
        return _fields.isEmpty();
      }

      // Unsupported operations.  These are not necessary for JSTL
      @Override
      public void clear() {
        throw new UnsupportedOperationException();
      }

      @Override
      public boolean containsValue(Object value) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Set<java.util.Map.Entry<String, Object>> entrySet() {
        throw new UnsupportedOperationException();
      }

      @Override
      public void putAll(Map<? extends String, ? extends Object> t) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Collection<Object> values() {
        throw new UnsupportedOperationException();
      }

      @Override
      public Collection<Object> put(String key, Object value) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Collection<Object> remove(Object key) {
        throw new UnsupportedOperationException();
      }

      @Override
      public String toString() {
        return _fields.toString();
      }
    };
  }

  // ---------------------------------------------------
  // MAP interface
  // ---------------------------------------------------

  @Override
  public boolean containsKey(Object key) {
    return _fields.containsKey(key);
  }

  @Override
  public boolean containsValue(Object value) {
    return _fields.containsValue(value);
  }

  @Override
  public Set<Entry<String, Object>> entrySet() {
    return _fields.entrySet();
  }

  // TODO: Shouldn't the input parameter here be a String?  The _fields map requires a String.
  @Override
  public Object get(Object key) {
    return _fields.get(key);
  }

  @Override
  public boolean isEmpty() {
    return _fields.isEmpty();
  }

  @Override
  public Set<String> keySet() {
    return _fields.keySet();
  }

  @Override
  public Object put(String key, Object value) {
    return _fields.put(key, value);
  }

  @Override
  public void putAll(Map<? extends String, ? extends Object> t) {
    _fields.putAll(t);
  }

  @Override
  public Object remove(Object key) {
    return _fields.remove(key);
  }

  @Override
  public int size() {
    return _fields.size();
  }

  @Override
  public Collection<Object> values() {
    return _fields.values();
  }

  @Override
  public void addChildDocument(SolrDocument child) {
    if (_childDocuments == null) {
      _childDocuments = new ArrayList<>();
    }
    _childDocuments.add(child);
  }

  @Override
  public void addChildDocuments(Collection<SolrDocument> children) {
    for (SolrDocument child : children) {
      addChildDocument(child);
    }
  }

  @Override
  public List<SolrDocument> getChildDocuments() {
    return _childDocuments;
  }

  @Override
  public boolean hasChildDocuments() {
    boolean isEmpty = (_childDocuments == null || _childDocuments.isEmpty());
    return !isEmpty;
  }

  @Override
  @Deprecated
  public int getChildDocumentCount() {
    if (_childDocuments == null) return 0;
    return _childDocuments.size();
  }
}
